/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.xmlrpc;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.net.URL;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;


/**
 * XML-RPC用のデータの加工を行うためのユーティリティクラス。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 * <br>
 * このクラスは、通常のXML-RPCとApache XML-RPCの両方に対応しています。<br>
 * ただし、Apache XML-RPC形式の拡張型のうち「ex:dom」と「ex:serializable」は使って欲しくないので、あえてサポートしていません。<br>
 * また、List以外のCollectionの場合も、配列と同じように扱います。<br>
 * <br>
 * 通常のXML-RPCでサポートしていない型をレスポンスして返す場合は、エラー扱いはせずになるべく近い型として返します。<br>
 * 特に、nullの場合は中身が空の「string」タグを返すので、注意して下さい。<br>
 * <br>
 * <strong>【参考ページ】</strong><br>
 * <a href="http://www.xmlrpc.com/spec" target="_blank">XML-RPCの仕様</a><br>
 * <a href="http://ws.apache.org/xmlrpc/types.html" target="_blank">Apache XML-RPCで拡張された型</a><br>
 * <a href="http://ja.wikipedia.org/wiki/XML-RPC" target="_blank">Wikipedia</a><br>
 */
public class XmlRpcUtil {
	//==========================================================
	//定数

	/**
	 * 拡張型の名前空間のURI
	 */
	public static final String EXTENSION_TYPE_NAMESPACE_URI = "http://ws.apache.org/xmlrpc/namespaces/extensions";

	/**
	 * 「dateTime.iso8601」タグのための日付フォーマッター
	 */
	//(このフォーマットではミリ秒やタイムゾーンの情報が失われてしまうが、そういう仕様なのであきらめる)
	private static SafeDateFormat datetimeIso8601_formatter = new SafeDateFormat("yyyyMMdd'T'HH:mm:ss");

	/**
	 * 「ex:dateTime」タグのための日付フォーマッター
	 */
	private static SafeDateFormat datetime_formatter = new SafeDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");


	//==========================================================
	//フィールド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスフィールド

	/**
	 * クラスに対する読み取り可能なプロパティ(publicフィールドとgetter)の情報をキャッシュするためのMap
	 */
	//(キャッシュなのでWeakHashMapにしても良いが、用途から考えてそれほど大量のメモリを使用するとは思えないのと、スレッドセーフ性を考慮して、ConcurrentHashMapにする)
	private static ConcurrentMap<Class<?>, Map<String, AnnotatedElement>> getPropertiesCacheMap = new ConcurrentHashMap<Class<?>, Map<String, AnnotatedElement>>();


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 * (クラスメソッドのみのユーティリティークラスなので、privateにしてインスタンスを作成できないようにしています。)<br>
	 */
	private XmlRpcUtil() {
		//基底クラスのコンストラクタの呼び出し
		super();
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//クラスメソッド

	/**
	 * XML-RPCの入力用のXMLの文字列をメソッド名と引数の情報を格納するためのDTOに変換します。<br>
	 * このメソッドは、通常のXML-RPCに加えて、Apache XML-RPCにも対応しています。<br>
	 *
	 * @param xmlStr XML-RPCの入力用のXMLの文字列
	 * @return メソッド名と引数の情報を格納するためのDTO
	 * @throws IOException 入出力エラーが発生した場合
	 * @throws SAXException SAXによる構文解析に失敗した場合
	 */
	public static MethodNameAndArgsDto inputXMLToDto(String xmlStr)
			throws IOException, SAXException {
		//引数の文字列をUTF-8のバイト配列に変換して、さらにInputStreamに変換
		InputStream is = new ByteArrayInputStream(xmlStr.getBytes("UTF-8"));

		try {
			//実際に処理を行うメソッドに処理を委譲する
			return inputXMLToDto(is);
		}
		//終了処理
		finally {
			try {
				//入力ストリームを確実に閉じる
				is.close();
			}
			//入力ストリームを閉じるのに失敗した場合
			catch (Exception e) {
				//どうしようもないので無視する
			}
		}
	}

	/**
	 * 入力ストリームから読み込んだXML-RPCの入力用のXMLの文字列をメソッド名と引数の情報を格納するためのDTOに変換します。<br>
	 * このメソッドは、通常のXML-RPCに加えて、Apache XML-RPCにも対応しています。<br>
	 *
	 * @param is 入力ストリーム
	 * @return メソッド名と引数の情報を格納するためのDTO
	 * @throws IOException 入出力エラーが発生した場合
	 * @throws SAXException SAXによる構文解析に失敗した場合
	 */
	public static MethodNameAndArgsDto inputXMLToDto(InputStream is)
			throws IOException, SAXException {
		try {
			//SAXParserFactoryのインスタンスを生成
			SAXParserFactory factory = SAXParserFactory.newInstance();

			//名前空間をサポートするようにする
			factory.setNamespaceAware(true);

			//ドキュメントの妥当性チェックは行わないようにする
			factory.setValidating(false);

			//SAXParserFactoryを使ってXMLReaderのインスタンスを取得
			XMLReader xmlReader = factory.newSAXParser().getXMLReader();

			//XML-RPCのリクエストデータの構文解析を行うクラスのインスタンスを生成
			XmlRpcRequestParser requestParser = new XmlRpcRequestParser();

			//XML-RPCのリクエストデータの構文解析を行うクラスのインスタンスをXMLReaderのContentHandlerとして設定
			xmlReader.setContentHandler(requestParser);

			//引数の入力ストリームを元にしてInputSourceのインスタンスを生成
			InputSource inputSource = new InputSource(is);

			//InputSourceの文字エンコーディングに「UTF-8」を設定
			inputSource.setEncoding("UTF-8");

			//SAXのパース処理を実行する
			xmlReader.parse(inputSource);


			//==========================================================
			//構文解析の結果をDTOに詰めて返す

			//メソッド名と引数の情報を格納するためのDTOのインスタンスを生成
			MethodNameAndArgsDto dto = new MethodNameAndArgsDto();

			//メソッド名に、「methodName」タグの内容の文字列を設定
			dto.methodName = requestParser.getMethodName();

			//引数の配列に、「params」タグの処理結果のオブジェクトの配列を設定
			dto.args = requestParser.getParams();

			//生成したDTOを返す
			return dto;
		}
		//Saxのパーサー自体の生成に失敗した場合
		catch (ParserConfigurationException e) {
			//捕捉した例外をSAXParseExceptionでラッピングして投げ直す
			throw new SAXException("Saxのパーサー自体の生成に失敗しました。", e);
		}
	}

	/**
	 * XML-RPCのレスポンス形式のXML文字列を作成して返します。<br>
	 *
	 * @param obj レスポンスとして返す内容のオブジェクト
	 * @return XML-RPCのレスポンス形式のXML文字列
	 * @throws IllegalArgumentException 出力データの値が不正な場合
	 */
	public static String createResponseXMLString(Object obj) {
		//作業用のStringBuilderのインスタンスを生成
		//(StringBuilderの内部バッファは最初から少し多めにとっておく)
		StringBuilder sb = new StringBuilder(1024);

		//固定部分の出力
		//(普通のXML-RPCなので、名前空間は出力しない)
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		sb.append("<methodResponse>");
		sb.append("<params>");
		sb.append("<param>");

		//レスポンスとして返す内容のオブジェクトを適切なXML-RPCのvalueタグとしてStringBuilderに追加する
		appendObjectToXMLValue(obj, sb);

		//固定部分の出力
		sb.append("</param>");
		sb.append("</params>");
		sb.append("</methodResponse>");

		//結果を文字列として返す
		return sb.toString();
	}

	/**
	 * Apache XML-RPCのレスポンス形式のXML文字列を作成して返します。<br>
	 *
	 * @param obj レスポンスとして返す内容のオブジェクト
	 * @return Apache XML-RPCのレスポンス形式のXML文字列
	 * @throws IllegalArgumentException 出力データの値が不正な場合
	 */
	public static String createResponseApacheXMLString(Object obj) {
		//作業用のStringBuilderのインスタンスを生成
		//(StringBuilderの内部バッファは最初から少し多めにとっておく)
		StringBuilder sb = new StringBuilder(1024);

		//固定部分の出力
		//(名前空間「ex」は個別に定義せずに、最初に一度だけ定義しておく)
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		sb.append("<methodResponse xmlns:ex=\"" + EXTENSION_TYPE_NAMESPACE_URI
				+ "\">");
		sb.append("<params>");
		sb.append("<param>");

		//レスポンスとして返す内容のオブジェクトを適切なApache XML-RPCのvalueタグとしてStringBuilderに追加する
		appendObjectToApacheXMLValue(obj, sb);

		//固定部分の出力
		sb.append("</param>");
		sb.append("</params>");
		sb.append("</methodResponse>");

		//結果を文字列として返す
		return sb.toString();
	}

	/**
	 * XML-RPCのエラーレスポンス形式のXML文字列を作成して返します。<br>
	 *
	 * @param errorCode エラーコード
	 * @param errorString エラー文字列
	 * @return XML-RPCのエラーレスポンス形式のXML文字列
	 */
	public static String createErrorResponseXMLString(int errorCode,
			String errorString) {
		//作業用のStringBuilderのインスタンスを生成
		//(StringBuilderの内部バッファは最初から少し多めにとっておく)
		StringBuilder sb = new StringBuilder(1024);

		//固定部分の出力
		//(普通のXML-RPCなので、名前空間は出力しない)
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		sb.append("<methodResponse>");
		sb.append("<fault>");


		//==========================================================
		//可変部分

		//エラー情報を出力するためのMapのインスタンスを生成
		//(キーの順番が文字コードの順になるようにするため、HashMapではなくTreeMapを使う)
		Map<String, Object> errorMap = new TreeMap<String, Object>();

		//faultCodeに引数のエラーコードを設定
		errorMap.put("faultCode", errorCode);

		//faultStringに引数のエラー文字列を設定
		errorMap.put("faultString", errorString);

		//エラー情報を出力するためのMapを適切なXML-RPCのvalueタグとしてStringBuilderに追加する
		appendObjectToXMLValue(errorMap, sb);
		//==========================================================


		//固定部分の出力
		sb.append("</fault>");
		sb.append("</methodResponse>");

		//結果を文字列として返す
		return sb.toString();
	}


	/**
	 * Apache XML-RPCのエラーレスポンス形式のXML文字列を作成して返します。<br>
	 *
	 * @param errorCode エラーコード
	 * @param errorString エラー文字列
	 * @return Apache XML-RPCのエラーレスポンス形式のXML文字列
	 */
	public static String createErrorResponseApacheXMLString(int errorCode,
			String errorString) {
		//作業用のStringBuilderのインスタンスを生成
		//(StringBuilderの内部バッファは最初から少し多めにとっておく)
		StringBuilder sb = new StringBuilder(1024);

		//固定部分の出力
		//(名前空間「ex」は個別に定義せずに、最初に一度だけ定義しておく)
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
		sb.append("<methodResponse xmlns:ex=\"" + EXTENSION_TYPE_NAMESPACE_URI
				+ "\">");
		sb.append("<fault>");


		//==========================================================
		//可変部分

		//エラー情報を出力するためのMapのインスタンスを生成
		//(キーの順番が文字コードの順になるようにするため、HashMapではなくTreeMapを使う)
		Map<String, Object> errorMap = new TreeMap<String, Object>();

		//faultCodeに引数のエラーコードを設定
		errorMap.put("faultCode", errorCode);

		//faultStringに引数のエラー文字列を設定
		errorMap.put("faultString", errorString);

		//エラー情報を出力するためのMapを適切なApache XML-RPCのvalueタグとしてStringBuilderに追加する
		appendObjectToApacheXMLValue(errorMap, sb);
		//==========================================================


		//固定部分の出力
		sb.append("</fault>");
		sb.append("</methodResponse>");

		//結果を文字列として返す
		return sb.toString();
	}


	//==========================================================
	//内部処理用

	/**
	 * 指定されたオブジェクトの内容を型に応じて適切なXML-RPCのvalueタグとしてStringBuilderに追加します。<br>
	 * ただし、staticフィールドは除外します。<br>
	 *
	 * @param obj オブジェクト
	 * @param sb 結果を出力するStringBuilder
	 * @throws IllegalArgumentException サポートされていない型のオブジェクトの場合
	 */
	private static void appendObjectToXMLValue(Object obj, StringBuilder sb) {
		//固定部分の出力
		sb.append("<value>");

		//引数のオブジェクトがnullの場合
		if (obj == null) {
			//普通のXML-RPCだとnilタグが使えないので、代わりに中身が空の「string」タグを追加する
			sb.append("<string/>");
		}
		//引数のオブジェクトがString, URL, URIの場合
		else if ((obj instanceof String) || (obj instanceof URL)
				|| (obj instanceof URI)) {
			//「string」タグを開始する
			sb.append("<string>");

			//オブジェクトの文字列表現をサニタイジングしてStringBuilderに追加する
			appendSanitiziedStringToXML(obj.toString(), sb);

			//「string」タグを閉じる
			sb.append("</string>");
		}
		//引数のオブジェクトがBooleanの場合
		else if (obj instanceof Boolean) {
			//「boolean」タグを開始する
			sb.append("<boolean>");

			//Booleanの値がtrueの場合
			if (((Boolean) obj).booleanValue()) {
				//「1」をStringBuilderに追加する
				sb.append('1');
			}
			//それ以外の場合
			else {
				//「0」をStringBuilderに追加する
				sb.append('0');
			}

			//「boolean」タグを閉じる
			sb.append("</boolean>");
		}
		//引数のオブジェクトがNumberの場合
		else if (obj instanceof Number) {
			//引数のオブジェクトがByte, Short, Integerの場合
			if ((obj instanceof Byte) || (obj instanceof Short)
					|| (obj instanceof Integer)) {
				//「i4」タグを開始する
				sb.append("<i4>");

				//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
				sb.append(obj.toString());

				//「i4」タグを閉じる
				sb.append("</i4>");
			}
			//それ以外の場合
			else {
				//「double」タグを開始する
				sb.append("<double>");

				//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
				sb.append(obj.toString());

				//「double」タグを閉じる
				sb.append("</double>");
			}
		}
		//引数のオブジェクトがjava.util.Date, Calendarの場合
		else if ((obj instanceof Date) || (obj instanceof Calendar)) {
			//Dateオブジェクト
			final Date date;

			//引数のオブジェクトがjava.util.Dateの場合
			if (obj instanceof Date) {
				//単純にDateオブジェクトにキャストする
				date = (Date) obj;
			}
			else {
				//CalendarをDateオブジェクトに変換
				date = ((Calendar) obj).getTime();
			}

			//「dateTime.iso8601」タグを開始する
			sb.append("<dateTime.iso8601>");

			//日付を整形してからStringBuilderに追加する
			sb.append(datetimeIso8601_formatter.format(date));

			//「dateTime.iso8601」タグを閉じる
			sb.append("</dateTime.iso8601>");
		}
		//引数のオブジェクトがバイト配列の場合
		else if (obj instanceof byte[]) {
			//「base64」タグを開始する
			sb.append("<base64>");

			//バイト配列をBase64形式の文字列に変換してからStringBuilderに追加する
			sb.append(Base64Util.byteArrayToBase64String((byte[]) obj));

			//「base64」タグを閉じる
			sb.append("</base64>");
		}
		//引数のオブジェクトがオブジェクトの配列、またはCollectionの場合
		else if ((obj instanceof Object[]) || (obj instanceof Collection)) {
			//「array」タグを開始する
			sb.append("<array>");

			//「data」タグを開始する
			sb.append("<data>");

			//オブジェクトの配列
			Object[] objArray = null;

			//引数のオブジェクトがオブジェクトの配列の場合
			if (obj instanceof Object[]) {
				//引数のオブジェクトをオブジェクトの配列にキャストして取得
				objArray = (Object[]) obj;
			}
			//それ以外の場合
			//(Collectionの場合)
			else {
				//引数のオブジェクトをCollectionにキャストして、さらにオブジェクトの配列に変換
				objArray = ((Collection) obj).toArray(new Object[0]);
			}

			//オブジェクトの配列の全ての要素に対して処理を行う
			for (Object o : objArray) {
				//現在のオブジェクトを引数に指定して、このメソッドを再帰的に呼び出す
				appendObjectToXMLValue(o, sb);
			}

			//「data」タグを閉じる
			sb.append("</data>");

			//「array」タグを閉じる
			sb.append("</array>");
		}
		//いままでのどの条件にも一致しなかった場合
		else {
			//出力対象のMap
			Map<String, Object> map = null;

			//有効なMapかどうかのフラグ
			boolean isMap = false;

			//引数のオブジェクトがMap場合
			if (obj instanceof Map) {
				//引数のオブジェクトをMapとしてそのまま使用する
				map = (Map) obj;

				//有効なMapかどうかのフラグをたてる
				isMap = true;
			}
			//それ以外の場合
			else {
				//引数のオブジェクトの読み取り可能なプロパティをMapに変換
				map = objectToMap(obj);

				//Mapの中身が空でない場合
				//(オブジェクトの読み取り可能なプロパティを持つオブジェクトだった場合)
				if (map.size() != 0) {
					//有効なMapかどうかのフラグをたてる
					isMap = true;
				}
			}

			//有効なMapの場合
			if (isMap) {
				//「struct」タグを開始する
				sb.append("<struct>");

				//Mapのエントリーセットに対してループをまわす
				for (Entry<String, Object> entry : map.entrySet()) {
					//「member」タグを開始する
					sb.append("<member>");

					//「name」タグを開始する
					sb.append("<name>");

					//エントリーのキーをサニタイジングしてからStringBuilderに追加する
					appendSanitiziedStringToXML(entry.getKey(), sb);

					//「name」タグを閉じる
					sb.append("</name>");

					//エントリーの値を引数に指定して、このメソッドを再帰的に呼び出す
					appendObjectToXMLValue(entry.getValue(), sb);

					//「member」タグを閉じる
					sb.append("</member>");
				}

				//「struct」タグを閉じる
				sb.append("</struct>");
			}
			//それ以外の場合
			//(publicフィールドがないオブジェクトの場合)
			else {
				//例外を投げる
				throw new IllegalArgumentException(obj.getClass().getName()
						+ "はサポートされていません。");
			}
		}

		//固定部分の出力
		sb.append("</value>");
	}

	/**
	 * 指定されたオブジェクトの内容を型に応じて適切なApache XML-RPCのvalueタグとしてStringBuilderに追加します。<br>
	 * ただし、staticフィールドは除外します。<br>
	 *
	 * @param obj オブジェクト
	 * @param sb 結果を出力するStringBuilder
	 * @throws IllegalArgumentException サポートされていない型のオブジェクトの場合
	 */
	private static void appendObjectToApacheXMLValue(Object obj,
			StringBuilder sb) {
		//固定部分の出力
		sb.append("<value>");

		//引数のオブジェクトがnullの場合
		if (obj == null) {
			//中身が空の「ex:nil」タグを追加する
			sb.append("<ex:nil/>");
		}
		//引数のオブジェクトがString, URL, URIの場合
		else if ((obj instanceof String) || (obj instanceof URL)
				|| (obj instanceof URI)) {
			//「string」タグを開始する
			sb.append("<string>");

			//オブジェクトの文字列表現をサニタイジングしてStringBuilderに追加する
			appendSanitiziedStringToXML(obj.toString(), sb);

			//「string」タグを閉じる
			sb.append("</string>");
		}
		//引数のオブジェクトがBooleanの場合
		else if (obj instanceof Boolean) {
			//「boolean」タグを開始する
			sb.append("<boolean>");

			//Booleanの値がtrueの場合
			if (((Boolean) obj).booleanValue()) {
				//「1」をStringBuilderに追加する
				sb.append('1');
			}
			//それ以外の場合
			else {
				//「0」をStringBuilderに追加する
				sb.append('0');
			}

			//「boolean」タグを閉じる
			sb.append("</boolean>");
		}
		//引数のオブジェクトがByteの場合
		else if (obj instanceof Byte) {
			//「ex:i1」タグを開始する
			sb.append("<ex:i1>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:i1」タグを閉じる
			sb.append("</ex:i1>");
		}
		//引数のオブジェクトがShortの場合
		else if (obj instanceof Short) {
			//「ex:i2」タグを開始する
			sb.append("<ex:i2>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:i2」タグを閉じる
			sb.append("</ex:i2>");
		}
		//引数のオブジェクトがIntegerの場合
		else if (obj instanceof Integer) {
			//「i4」タグを開始する
			sb.append("<i4>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「i4」タグを閉じる
			sb.append("</i4>");
		}
		//引数のオブジェクトがLongの場合
		else if (obj instanceof Long) {
			//「ex:i8」タグを開始する
			sb.append("<ex:i8>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:i8」タグを閉じる
			sb.append("</ex:i8>");
		}
		//引数のオブジェクトがFloatの場合
		else if (obj instanceof Float) {
			//「ex:float」タグを開始する
			sb.append("<ex:float>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:float」タグを閉じる
			sb.append("</ex:float>");
		}
		//引数のオブジェクトがDoubleの場合
		else if (obj instanceof Double) {
			//「double」タグを開始する
			sb.append("<double>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「double」タグを閉じる
			sb.append("</double>");
		}
		//引数のオブジェクトがBigIntegerの場合
		else if (obj instanceof BigInteger) {
			//「ex:biginteger」タグを開始する
			sb.append("<ex:biginteger>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:biginteger」タグを閉じる
			sb.append("</ex:biginteger>");
		}
		//引数のオブジェクトがBigIntegerの場合
		else if (obj instanceof BigInteger) {
			//「ex:biginteger」タグを開始する
			sb.append("<ex:biginteger>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:biginteger」タグを閉じる
			sb.append("</ex:biginteger>");
		}
		//引数のオブジェクトがBigDecimalの場合
		else if (obj instanceof BigDecimal) {
			//「ex:bigdecimal」タグを開始する
			sb.append("<ex:bigdecimal>");

			//オブジェクトの内容を文字列に変換して、そのままStringBuilderに追加する
			sb.append(obj.toString());

			//「ex:bigdecimal」タグを閉じる
			sb.append("</ex:bigdecimal>");
		}
		//引数のオブジェクトがjava.util.Dateの場合
		else if (obj instanceof Date) {
			//「dateTime.iso8601」タグを開始する
			sb.append("<dateTime.iso8601>");

			//日付を整形してからStringBuilderに追加する
			sb.append(datetimeIso8601_formatter.format((Date) obj));

			//「dateTime.iso8601」タグを閉じる
			sb.append("</dateTime.iso8601>");
		}
		//引数のオブジェクトがCalendarの場合
		else if (obj instanceof Calendar) {
			//「ex:dateTime」タグを開始する
			sb.append("<ex:dateTime>");

			//CalendarをDateオブジェクトに変換
			Date date = ((Calendar) obj).getTime();

			//日付を整形してからStringBuilderに追加する
			sb.append(datetime_formatter.format(date));

			//最後から2文字目の位置に「:」を挿入
			//(タイムゾーン部分を「±99:99」形式に変換するため)
			sb.insert(sb.length() - 2, ":");

			//「ex:dateTime」タグを閉じる
			sb.append("</ex:dateTime>");
		}
		//引数のオブジェクトがバイト配列の場合
		else if (obj instanceof byte[]) {
			//「base64」タグを開始する
			sb.append("<base64>");

			//バイト配列をBase64形式の文字列に変換してからStringBuilderに追加する
			sb.append(Base64Util.byteArrayToBase64String((byte[]) obj));

			//「base64」タグを閉じる
			sb.append("</base64>");
		}
		//引数のオブジェクトがオブジェクトの配列、またはCollectionの場合
		else if ((obj instanceof Object[]) || (obj instanceof Collection)) {
			//「array」タグを開始する
			sb.append("<array>");

			//「data」タグを開始する
			sb.append("<data>");

			//オブジェクトの配列
			Object[] objArray = null;

			//引数のオブジェクトがオブジェクトの配列の場合
			if (obj instanceof Object[]) {
				//引数のオブジェクトをオブジェクトの配列にキャストして取得
				objArray = (Object[]) obj;
			}
			//それ以外の場合
			//(Collectionの場合)
			else {
				//引数のオブジェクトをCollectionにキャストして、さらにオブジェクトの配列に変換
				objArray = ((Collection) obj).toArray(new Object[0]);
			}

			//オブジェクトの配列の全ての要素に対して処理を行う
			for (Object o : objArray) {
				//現在のオブジェクトを引数に指定して、このメソッドを再帰的に呼び出す
				appendObjectToApacheXMLValue(o, sb);
			}

			//「data」タグを閉じる
			sb.append("</data>");

			//「array」タグを閉じる
			sb.append("</array>");
		}
		//いままでのどの条件にも一致しなかった場合
		else {
			//出力対象のMap
			Map<String, Object> map = null;

			//有効なMapかどうかのフラグ
			boolean isMap = false;

			//引数のオブジェクトがMap場合
			if (obj instanceof Map) {
				//引数のオブジェクトをMapとしてそのまま使用する
				map = (Map) obj;

				//有効なMapかどうかのフラグをたてる
				isMap = true;
			}
			//それ以外の場合
			else {
				//引数のオブジェクトの読み取り可能なプロパティをMapに変換
				map = objectToMap(obj);

				//Mapの中身が空でない場合
				//(オブジェクトの読み取り可能なプロパティを持つオブジェクトだった場合)
				if (map.size() != 0) {
					//有効なMapかどうかのフラグをたてる
					isMap = true;
				}
			}

			//有効なMapの場合
			if (isMap) {
				//「struct」タグを開始する
				sb.append("<struct>");

				//Mapのエントリーセットに対してループをまわす
				for (Entry<String, Object> entry : map.entrySet()) {
					//「member」タグを開始する
					sb.append("<member>");

					//「name」タグを開始する
					sb.append("<name>");

					//エントリーのキーをサニタイジングしてからStringBuilderに追加する
					appendSanitiziedStringToXML(entry.getKey(), sb);

					//「name」タグを閉じる
					sb.append("</name>");

					//エントリーの値を引数に指定して、このメソッドを再帰的に呼び出す
					appendObjectToApacheXMLValue(entry.getValue(), sb);

					//「member」タグを閉じる
					sb.append("</member>");
				}

				//「struct」タグを閉じる
				sb.append("</struct>");
			}
			//それ以外の場合
			//(publicフィールドがないオブジェクトの場合)
			else {
				//例外を投げる
				throw new IllegalArgumentException(obj.getClass().getName()
						+ "はサポートされていません。");
			}
		}

		//固定部分の出力
		sb.append("</value>");
	}

	/**
	 * 指定された文字列の内容をXML用にサニタイジングしつつ、StringBuilderに追加します。<br>
	 *
	 * @param str 文字列
	 * @param sb 結果を出力するStringBuilder
	 */
	private static void appendSanitiziedStringToXML(String str, StringBuilder sb) {
		//インデックス
		int index = 0;

		//文字列の長さ
		int length = str.length();

		//インデックスが文字列の長さに達するまでループをまわす
		while (index < length) {
			//文字列のインデックスの位置のコードポイントを取得
			//(charだと扱えない文字があるので、JDK 1.5からはコードポイントを使う方が良いらしいため)
			int codePoint = str.codePointAt(index);

			//「&」の場合
			if (codePoint == '&') {
				//代わりに「&amp;」を追加
				sb.append("&amp;");
			}
			//「<」の場合
			else if (codePoint == '<') {
				//代わりに「&lt;」を追加
				sb.append("&lt;");
			}
			//「>」の場合
			else if (codePoint == '>') {
				//代わりに「&gt;」を追加
				sb.append("&gt;");
			}
			//「"」の場合
			else if (codePoint == '"') {
				//代わりに「&quot;」を追加
				sb.append("&quot;");
			}
			//「'」の場合
			else if (codePoint == '\'') {
				//代わりに「&apos;」を追加
				sb.append("&apos;");
			}
			//「\r」の場合
			else if (codePoint == '\r') {
				//代わりに「&#13;」を追加
				sb.append("&#13;");
			}
			//どの特殊文字にもあてはまらない場合
			else {
				//コードポイントをそのまま追加
				sb.appendCodePoint(codePoint);
			}

			//コードポイントをcharとして換算したカウントの分だけインデックスを進める
			index += Character.charCount(codePoint);
		}

	}

	/**
	 * 指定されたオブジェクトの読み取り可能なプロパティ(publicフィールドとgetter)の内容をMapとして返します。<br>
	 * ただし、staticな要素の場合は除外します。<br>
	 *
	 * @param obj オブジェクト
	 * @return オブジェクトの読み取り可能なプロパティの内容を格納したMap
	 */
	private static Map<String, Object> objectToMap(Object obj) {
		try {
			//結果を格納するMap
			//(キーの順番が文字コードの順になるようにするため、HashMapではなくTreeMapを使う)
			Map<String, Object> map = new TreeMap<String, Object>();

			//指定されたオブジェクトのクラスのpublicフィールドおよびgetterの情報をMapとして取得
			Map<String, AnnotatedElement> getPropertiesMap = getGetPropertiesMap(obj.getClass());

			//publicフィールドおよびgetterの情報のMapのエントリーセットに対してループをまわす
			for (Entry<String, AnnotatedElement> entry : getPropertiesMap.entrySet()) {
				//エントリーの値(FieldまたはMethod)を取得
				AnnotatedElement fieldOrMethod = entry.getValue();

				//オブジェクトの値
				final Object value;

				//エントリーの型がFieldの場合
				if (fieldOrMethod instanceof Field) {
					//オブジェクトの値をFieldから取得
					value = ((Field) fieldOrMethod).get(obj);
				}
				//それ以外の場合
				//(エントリーの型がMethodの場合)
				else {
					//オブジェクトの値をMethodから取得
					value = ((Method) fieldOrMethod).invoke(obj);
				}

				//エントリーのキー(プロパティ名)とオブジェクトの値を結果を格納するMapに登録する
				map.put(entry.getKey(), value);
			}

			//結果を格納するMapを返す
			return map;
		}
		//例外処理
		catch (Exception e) {
			//捕捉した例外をRuntimeExceptionでラッピングして投げ直す
			throw new RuntimeException("ObjectをMapに変換する処理に失敗しました。", e);
		}
	}

	/**
	 * 指定されたクラスの読み取り可能なプロパティ(publicフィールドとgetter)の情報をMapとして返します。<br>
	 * ただし、staticな要素の場合は除外します。<br>
	 *
	 * @param clazz クラスオブジェクト
	 * @return publicおよびgetterの内容を格納したMap(値はFieldまたはMethod)
	 * @throws IllegalAccessException フィールドまたはメソッドへのアクセスが失敗した場合
	 */
	private static Map<String, AnnotatedElement> getGetPropertiesMap(
			Class<?> clazz) throws IllegalAccessException {
		//クラスに対する読み取り可能なプロパティ(publicフィールドとgetter)の情報をキャッシュするためのMapから取得を試みる
		Map<String, AnnotatedElement> map = getPropertiesCacheMap.get(clazz);

		//キャッシュから取得できなかった場合
		if (map == null) {
			//結果を格納するMapを生成
			map = new HashMap<String, AnnotatedElement>();


			//==========================================================
			//publicフィールド
			{
				//引数のクラスの全てのpublicフィールドにアクセスするために、Fieldオブジェクトの配列を取得
				Field[] fieldArray = clazz.getFields();

				//Fieldオブジェクトの配列の全ての要素に対して処理を行う
				for (Field field : fieldArray) {
					//staticフィールド、または合成フィールドの場合
					if (((field.getModifiers() & Modifier.STATIC) != 0)
							|| field.isSynthetic()) {
						//次の要素に処理を進める
						continue;
					}

					//結果を格納するMapにフィールド名とFieldオブジェクト自体を登録する
					map.put(field.getName(), field);
				}
			}


			//==========================================================
			//getter
			{
				//指定されたオブジェクトの全てのメソッドにアクセスするために、Methodオブジェクトの配列を取得
				Method[] methodArray = clazz.getMethods();

				//Methodオブジェクトの配列の全ての要素に対して処理を行う
				for (Method method : methodArray) {
					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
					//getterと見なせるメソッドかチェック

					//Objectクラスで定義されているメソッドの場合
					//(toString()やgetClass()など)
					if (method.getDeclaringClass().equals(Object.class)) {
						//次の要素に処理を進める
						continue;
					}

					//staticメソッド、合成メソッド、またはブリッジメソッドの場合
					//(特殊なメソッドを除外するため)
					if (((method.getModifiers() & Modifier.STATIC) != 0)
							|| method.isSynthetic() || method.isBridge()) {
						//次の要素に処理を進める
						continue;
					}

					//引数の数が0でない場合
					//(引数をとるメソッドを除外するため)
					if (method.getParameterTypes().length != 0) {
						//次の要素に処理を進める
						continue;
					}

					//メソッド名を取得
					String methodName = method.getName();

					//プレフィックスを除いたメソッド名の開始位置
					//(プロパティ名を抽出するため)
					int start = 0;

					//getXxxメソッドの場合
					//(メソッド名がgetで始まり、3文字より長く、4文字目が大文字で、戻り値の型がvoidでない場合)
					if (methodName.startsWith("get")
							&& (methodName.length() > 3)
							&& Character.isUpperCase(methodName.charAt(3))
							&& !method.getReturnType().equals(void.class)) {
						//プレフィックスを除いたメソッド名の開始位置に3を設定
						start = 3;
					}
					//isXxxメソッドの場合
					//(メソッド名がisで始まり、2文字より長く、3文字目が大文字で、戻り値の型がbooleanまたはBooleanの場合)
					else if (methodName.startsWith("is")
							&& (methodName.length() > 2)
							&& Character.isUpperCase(methodName.charAt(2))
							&& (method.getReturnType().equals(boolean.class) || method.getReturnType().equals(Boolean.class))) {
						//プレフィックスを除いたメソッド名の開始位置に2を設定
						start = 2;
					}
					//それ以外の場合
					//(getterと見なせないメソッドの場合)
					else {
						//次の要素に処理を進める
						continue;
					}


					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
					//プロパティ名を取得

					//メソッド名からプレフィックスの部分を除外して、プロパティ名を取得
					String propertyName = methodName.substring(start);

					//プロパティ名が1文字の場合、または2文字目が大文字でない場合
					if ((propertyName.length() == 1)
							|| !Character.isUpperCase(propertyName.charAt(1))) {
						//プロパティ名の1文字目を小文字にする
						char[] chars = propertyName.toCharArray();
						chars[0] = Character.toLowerCase(chars[0]);
						propertyName = new String(chars);
					}
					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/


					//結果を格納するMapにプロパティ名とMethodオブジェクト自体を登録する
					map.put(propertyName, method);
				}
			}


			//==========================================================
			//キャッシュへの登録

			//作成したMapをキャッシュ用のMapにアトミックに登録する
			Map<String, AnnotatedElement> originalMap = getPropertiesCacheMap.putIfAbsent(clazz, map);

			//Mapへの登録時の戻り値がnullでない場合
			//(すでに別の値が登録されていた場合)
			if (originalMap != null) {
				//すでに登録されていた値を採用する
				map = originalMap;
			}
		}

		//キャッシュから取得、または新たに生成したMapを返す
		return map;
	}
}
